namespace Gma.QrCodeNet.Encoding.ReedSolomon;

internal sealed class Polynomial
{
	internal Polynomial(GaloisField256 gfield, int[] coefficients)
	{
		int coefficientsLength = coefficients.Length;

		if (coefficientsLength == 0 || coefficients is null)
		{
			throw new ArithmeticException($"Cannot create empty {nameof(Polynomial)}.");
		}

		GField = gfield;

		Primitive = gfield.Primitive;

		if (coefficientsLength > 1 && coefficients[0] == 0)
		{
			int firstNonZeroIndex = 1;
			while (firstNonZeroIndex < coefficientsLength && coefficients[firstNonZeroIndex] == 0)
			{
				firstNonZeroIndex++;
			}

			if (firstNonZeroIndex == coefficientsLength)
			{
				Coefficients = new int[] { 0 };
			}
			else
			{
				int newLength = coefficientsLength - firstNonZeroIndex;
				Coefficients = new int[newLength];
				Array.Copy(coefficients, firstNonZeroIndex, Coefficients, 0, newLength);
			}
		}
		else
		{
			Coefficients = new int[coefficientsLength];
			Array.Copy(coefficients, Coefficients, coefficientsLength);
		}
	}

	internal int[] Coefficients { get; }

	internal GaloisField256 GField { get; }

	internal int Degree => Coefficients.Length - 1;

	internal int Primitive { get; }

	internal bool IsMonomialZero => Coefficients[0] == 0;

	/// <returns>
	/// Coefficient position. where (coefficient)x^degree
	/// </returns>
	internal int GetCoefficient(int degree)
	{
		// Eg: x^2 + x + 1. degree 1, reverse position = degree + 1 = 2.
		// Pos = 3 - 2 = 1
		return Coefficients[^(degree + 1)];
	}

	/// <summary>
	/// Add another Polynomial to current one
	/// </summary>
	/// <param name="other">The polynomial need to add or subtract to current one</param>
	/// <returns>Result polynomial after add or subtract</returns>
	internal Polynomial AddOrSubtract(Polynomial other)
	{
		if (Primitive != other.Primitive)
		{
			throw new ArgumentException($"{nameof(Polynomial)} cannot perform {nameof(AddOrSubtract)} as they do not have the same {nameof(Primitive)}" +
				$" for {nameof(GaloisField256)}.");
		}
		if (IsMonomialZero)
		{
			return other;
		}
		else if (other.IsMonomialZero)
		{
			return this;
		}

		int otherLength = other.Coefficients.Length;
		int thisLength = Coefficients.Length;

		if (otherLength > thisLength)
		{
			return CoefficientXor(Coefficients, other.Coefficients);
		}
		else
		{
			return CoefficientXor(other.Coefficients, Coefficients);
		}
	}

	internal Polynomial CoefficientXor(int[] smallerCoefficients, int[] largerCoefficients)
	{
		if (smallerCoefficients.Length > largerCoefficients.Length)
		{
			throw new ArgumentException($"Cannot perform {nameof(CoefficientXor)} method as smaller {nameof(Coefficients)} length is greater than the larger one.");
		}

		int targetLength = largerCoefficients.Length;
		int[] xorCoefficient = new int[targetLength];
		int lengthDiff = largerCoefficients.Length - smallerCoefficients.Length;

		Array.Copy(largerCoefficients, 0, xorCoefficient, 0, lengthDiff);

		for (int index = lengthDiff; index < targetLength; index++)
		{
			xorCoefficient[index] = GField.Addition(largerCoefficients[index], smallerCoefficients[index - lengthDiff]);
		}

		return new Polynomial(GField, xorCoefficient);
	}

	/// <summary>
	/// Multiply current Polynomial to another one.
	/// </summary>
	/// <returns>Result polynomial after multiply</returns>
	internal Polynomial Multiply(Polynomial other)
	{
		if (Primitive != other.Primitive)
		{
			throw new ArgumentException($"{nameof(Polynomial)} cannot perform {nameof(Multiply)} as they do not have the same {nameof(Primitive)}" +
				$" for {nameof(GaloisField256)}.");
		}
		if (IsMonomialZero || other.IsMonomialZero)
		{
			return new Polynomial(GField, new int[] { 0 });
		}

		int[] aCoefficients = Coefficients;
		int aLength = aCoefficients.Length;
		int[] bCoefficient = other.Coefficients;
		int bLength = bCoefficient.Length;
		int[] rCoefficients = new int[aLength + bLength - 1];

		for (int aIndex = 0; aIndex < aLength; aIndex++)
		{
			int aCoeff = aCoefficients[aIndex];
			for (int bIndex = 0; bIndex < bLength; bIndex++)
			{
				rCoefficients[aIndex + bIndex] =
					GField.Addition(rCoefficients[aIndex + bIndex], GField.Product(aCoeff, bCoefficient[bIndex]));
			}
		}
		return new Polynomial(GField, rCoefficients);
	}

	/// <summary>
	/// Multiplay scalar to current polynomial
	/// </summary>
	/// <returns>Result of polynomial after multiply scalar</returns>
	internal Polynomial MultiplyScalar(int scalar)
	{
		if (scalar == 0)
		{
			return new Polynomial(GField, new int[] { 0 });
		}
		else if (scalar == 1)
		{
			return this;
		}

		int length = Coefficients.Length;
		int[] rCoefficient = new int[length];

		for (int index = 0; index < length; index++)
		{
			rCoefficient[index] = GField.Product(Coefficients[index], scalar);
		}

		return new Polynomial(GField, rCoefficient);
	}

	/// <summary>
	/// Divide current polynomial by "other"
	/// </summary>
	/// <returns>Result polynomial after divide</returns>
	internal PolyDivideStruct Divide(Polynomial other)
	{
		if (Primitive != other.Primitive)
		{
			throw new ArgumentException($"{nameof(Polynomial)} cannot perform {nameof(Divide)} as they do not have the same {nameof(Primitive)}" +
				$" for {nameof(GaloisField256)}.");
		}
		if (other.IsMonomialZero)
		{
			throw new ArgumentException($"Cannot divide by {nameof(Polynomial)} Zero.");
		}

		// This divide by other = a divide by b
		int aLength = Coefficients.Length;

		// We will make change to aCoefficient. It will return as remainder
		int[] aCoefficients = new int[aLength];
		Array.Copy(Coefficients, 0, aCoefficients, 0, aLength);

		int bLength = other.Coefficients.Length;

		if (aLength < bLength)
		{
			return new PolyDivideStruct(new Polynomial(GField, new int[] { 0 }), this);
		}
		else
		{
			// Quotient coefficients
			// qLastIndex = alength - blength  qlength = qLastIndex + 1
			int[] qCoefficients = new int[(aLength - bLength) + 1];

			// Denominator
			int otherLeadingTerm = other.GetCoefficient(other.Degree);
			int inverseOtherLeadingTerm = GField.Inverse(otherLeadingTerm);

			for (int aIndex = 0; aIndex <= aLength - bLength; aIndex++)
			{
				if (aCoefficients[aIndex] != 0)
				{
					int aScalar = GField.Product(inverseOtherLeadingTerm, aCoefficients[aIndex]);
					Polynomial term = other.MultiplyScalar(aScalar);
					qCoefficients[aIndex] = aScalar;

					int[] bCoefficient = term.Coefficients;
					if (bCoefficient[0] != 0)
					{
						for (int bIndex = 0; bIndex < bLength; bIndex++)
						{
							aCoefficients[aIndex + bIndex] = GField.Subtraction(aCoefficients[aIndex + bIndex], bCoefficient[bIndex]);
						}
					}
				}
			}

			return new PolyDivideStruct(new Polynomial(GField, qCoefficients), new Polynomial(GField, aCoefficients));
		}
	}
}
